<?php
/**
 * Svick a Fast Simple Smart PHP FrameWork
 * Author: Tommy 863758705@qq.com
 * Link: http://svick.tomener.com/
 * Since: 2022
 */

namespace Svick\Http\Client;


use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Pool;
use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Promise\Utils;

class Request
{
    /**
     * \GuzzleHttp\Client单例
     * @var array
     */
    private static $instances = [];

    /**
     * \GuzzleHttp\Client;
     * @var Client
     */
    protected $client;

    protected $async = false;

    protected $concurrent = false;

    /**
     * @var string
     */
    protected $bodyFormat = 'json';

    /**
     * @var array
     */
    protected $options = [];

    /**
     * @var array
     */
    protected $pendingFiles = [];

    /**
     * @var array
     */
    protected $promises = [];

    /**
     * @var bool 是否使用Pool
     */
    protected $pool = false;

    /**
     * @var int 并发次数
     */
    protected $concurrency;

    public function __construct()
    {
        $this->client = $this->getInstance();

        $this->options    = [
            'http_errors' => false,
        ];
    }

    /**
     * 获取单例
     *
     * @return mixed
     */
    public function getInstance()
    {
        $name = get_called_class();

        if (!isset(self::$instances[$name])) {
            self::$instances[$name] = new Client();
        }

        return self::$instances[$name];
    }

    public function asForm()
    {
        $this->bodyFormat('form_params');
        //$this->withHeaders(['Content-Type' => 'application/x-www-form-urlencoded']);

        return $this;
    }

    public function asJson()
    {
        $this->bodyFormat('json');
        //$this->withHeaders(['Content-Type' => 'application/json']);

        return $this;
    }

    /**
     * @param array $headers
     * @param null $value
     * @return $this
     */
    public function withHeaders(array $headers)
    {
        $this->options = array_merge_recursive($this->options, [
            'headers' => $headers,
        ]);

        return $this;
    }

    public function withOptions(array $options)
    {
        $this->options = array_merge_recursive($this->options, $options);

        return $this;
    }

    public function withCert(string $path, string $password)
    {
        $this->options['cert'] = [$path, $password];

        return $this;
    }

    public function withBasicAuth(string $username, string $password)
    {
        $this->options['auth'] = [$username, $password];

        return $this;
    }

    public function withDigestAuth(string $username, string $password)
    {
        $this->options['auth'] = [$username, $password, 'digest'];

        return $this;
    }

    public function withUA(string $ua)
    {
        $this->options['headers']['User-Agent'] = trim($ua);

        return $this;
    }

    public function withToken(string $token, string $type = 'Bearer')
    {
        $this->options['headers']['Authorization'] = trim($type . ' ' . $token);

        return $this;
    }

    public function withCookies(array $cookies, string $domain)
    {
        $this->options = array_merge_recursive($this->options, [
            'cookies' => CookieJar::fromArray($cookies, $domain),
        ]);

        return $this;
    }

    public function withProxy($proxy)
    {
        $this->options['proxy'] = $proxy;

        return $this;
    }

    public function withVersion($version)
    {
        $this->options['version'] = $version;

        return $this;
    }

    public function withRedirect($redirect = false)
    {
        $this->options['allow_redirects'] = $redirect;

        return $this;
    }

    public function withVerify($verify = false)
    {
        $this->options['verify'] = $verify;

        return $this;
    }

    public function withStream($boolean = false)
    {
        $this->options['stream'] = $boolean;

        return $this;
    }

    public function concurrency(int $num)
    {
        $this->concurrency = $num;

        return $this;
    }

    public function delay(int $seconds)
    {
        $this->options['delay'] = $seconds * 1000;

        return $this;
    }

    public function timeout(int $seconds)
    {
        $this->options['timeout'] = $seconds;

        return $this;
    }

    public function attach(string $name, $contents, string $filename = null, array $headers = [])
    {
        if (is_array($name)) {
            foreach ($name as $file) {
                $this->attach(...$file);
            }

            return $this;
        }

        if (!isset($this->options['multipart'])) {
            $this->bodyFormat('multipart');
            $this->options['multipart'] = [];
        }

        $this->options['multipart'][] = array_filter([
            'name' => $name,
            'contents' => $contents,
            'headers' => $headers,
            'filename' => $filename,
        ]);

        return $this;
    }

    public function get(string $url, array $query = [])
    {
        $this->options['query'] = $query;

        return $this->request('GET', $url);
    }

    public function post(string $url, array $data = [])
    {
        if ($this->bodyFormat != 'multipart') {
            $this->options[$this->bodyFormat] = $data;
        }

        return $this->request('POST', $url);
    }

    public function patch(string $url, array $data = [])
    {
        $this->options[$this->bodyFormat] = $data;

        return $this->request('PATCH', $url);
    }

    public function put(string $url, array $data = [])
    {
        $this->options[$this->bodyFormat] = $data;

        return $this->request('PUT', $url);
    }

    public function delete(string $url, array $data = [])
    {
        $this->options[$this->bodyFormat] = $data;

        return $this->request('DELETE', $url);
    }

    public function head(string $url, array $data = [])
    {
        $this->options[$this->bodyFormat] = $data;

        return $this->request('HEAD', $url);
    }

    public function options(string $url, array $data = [])
    {
        $this->options[$this->bodyFormat] = $data;

        return $this->request('OPTIONS', $url);
    }

    /**
     * @param $promises
     * @return $this
     */
    public function concurrent(array $promises)
    {
        $this->promises = $promises;
        return $this;
    }

    /**
     * @param array|callable $promises
     * @return $this
     */
    public function pool($promises)
    {
        $this->promises = $promises;
        $this->pool = true;
        return $this;
    }

    /**
     * 发送请求
     *
     * @param string $method
     * @param string $url
     * @return Response|Promise|PromiseInterface
     * @throws ConnectionException
     */
    protected function request(string $method, string $url)
    {
        try {
            $ret = parse_url($url);
            $query_string = $ret['query'] ?? '';
            if (!empty($query_string)) {
                parse_str($query_string, $query);
                $this->options['query'] = !isset($this->options['query']) ? $query : $this->options['query'] + $query;
            }
            if (!$this->async) {
                $response = $this->client->request($method, $url, $this->options);
                return $this->response($response);
            }

            if ($this->concurrent) {
                return $this->client->requestAsync($method, $url, $this->options);
            }

            return new Promise($this->client->requestAsync($method, $url, $this->options));
        } catch (GuzzleException $e) {
            throw new ConnectionException($e->getMessage(), 0, $e);
        }
    }

    /**
     * 设置同步或者异步请求
     *
     * @param bool $async
     * @return $this
     */
    public function async(bool $async = true)
    {
        $this->async = $async;
        return $this;
    }

    /**
     * @return array|mixed
     */
    public function wait()
    {
        if (empty($this->promises) && !is_callable($this->promises)) {
            return [];
        }

        $this->concurrency = $this->concurrency ? : count($this->promises);

        if (!is_callable($this->promises)) {
            $requests = function () {
                foreach ($this->promises as $promise) {
                    yield function() use ($promise) {
                        return $promise;
                    };
                }
            };
        } else {
            $requests = $this->promises;
        }

        if ($this->pool) {
            $responses = [];
            $pool = new Pool($this->client, $requests(), [
                'concurrency' => $this->concurrency,
                'fulfilled' => function (\GuzzleHttp\Psr7\Response $response, $index) use (&$responses) {
                    $responses[$index] = new Response($response);
                },
                'rejected' => function (\GuzzleHttp\Exception\RequestException $reason, $index) use (&$responses) {
                    $responses[$index] = new Response($reason->getResponse());
                },
            ]);

            $promise = $pool->promise();
            $promise->wait();
            return $responses;
        }

        $responses = Utils::settle($this->promises)->wait();
        foreach ($responses as $key => $response) {
            if ($response['state'] == 'fulfilled') {
                $responses[$key] = new Response($response['value']);
            } else {
                $responses[$key] = new Response(new \GuzzleHttp\Psr7\Response(500));
            }
        }

        return $responses;
    }

    public function co(bool $concurrent = true)
    {
        $this->concurrent = $concurrent;
        $this->async();

        return $this;
    }

    public function bodyFormat($format)
    {
        $this->bodyFormat = $format;
    }

    protected function response($response)
    {
        return new Response($response);
    }

    protected function exception($exception)
    {
        return new RequestException($exception);
    }
}
